<!DOCTYPE html>
<meta charset="utf-8">
<title>ScrollTimeline constructor</title>
  <link rel="help" href="https://wicg.github.io/scroll-animations/#scrolltimeline-interface">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<style>
.scroller {
  height: 100px;
  width: 100px;
  overflow: scroll;
}

.content {
  height: 500px;
  width: 500px;
}
</style>

<div class="scroller">
  <div class="content"></div>
</div>

<script>
'use strict';

function formatOffset(v) {
  if (typeof(v) == 'object')
    return `${v.constructor.name}(${v.toString()})`;
  return `'${v.toString()}'`;
}

function assert_offsets_equal(a, b) {
  assert_equals(formatOffset(a), formatOffset(b));
}

// TODO(smcgruer): In many of the tests below, timeRange is specified when it
// should not need to be. This is an artifact of the initial Chrome
// implementation which doesn't support timeRange: 'auto'. These should be
// removed in the future.

// scrollSource

test(t => {
  const scroller = document.querySelector('.scroller');
  assert_equals(
      new ScrollTimeline({scrollSource: scroller, timeRange: 100}).scrollSource,
      scroller);
}, 'A ScrollTimeline can be created with a scrollSource');

test(t => {
  const div = document.createElement('div');
  assert_equals(
      new ScrollTimeline({scrollSource: div, timeRange: 100}).scrollSource,
      div);
}, 'A ScrollTimeline can be created with a non-scrolling scrollSource');

test(t => {
  assert_equals(
      new ScrollTimeline({scrollSource: null, timeRange: 100}).scrollSource,
      null);
}, 'A ScrollTimeline created with a null scrollSource should have no scrollSource');

test(t => {
  assert_equals(
      new ScrollTimeline({timeRange: 100}).scrollSource,
      document.scrollingElement);
}, 'A ScrollTimeline created without a scrollSource should use the document.scrollingElement');

// orientation

test(t => {
  assert_equals(new ScrollTimeline({timeRange: 100}).orientation, 'block');
}, 'A ScrollTimeline created with the default orientation should default to \'block\'');

const gValidOrientationValues = [
  'block',
  'inline',
  'horizontal',
  'vertical',
];

for (let orientation of gValidOrientationValues) {
  test(function() {
    const scrollTimeline =
        new ScrollTimeline({orientation: orientation, timeRange: 100});
    assert_equals(scrollTimeline.orientation, orientation);
  }, '\'' + orientation + '\' is a valid orientation value');
}

test(t => {
  let constructorFunc = function() {
    new ScrollTimeline({orientation: 'nonsense', timeRange: 100})
  };
  assert_throws_js(TypeError, constructorFunc);

  // 'auto' for orientation was previously in the spec, but was removed. Make
  // sure that implementations do not support it.
  constructorFunc = function() {
    new ScrollTimeline({orientation: 'auto', timeRange: 100})
  };
  assert_throws_js(TypeError, constructorFunc);
}, 'Creating a ScrollTimeline with an invalid orientation value should throw');

// scrollOffsets

function formatOffset(v) {
  if (typeof(v) == 'object')
    return `${v.constructor.name}(${v.toString()})`;
  return `'${v.toString()}'`;
}

function assert_offsets_equal(a, b) {
  assert_equals(formatOffset(a), formatOffset(b));
}

test(t => {
  assert_array_equals(new ScrollTimeline({timeRange: 100}).scrollOffsets, []);
}, 'A ScrollTimeline created with the default scrollOffsets should default to []');

test(t => {
  assert_array_equals(new ScrollTimeline({timeRange: 100, scrollOffsets: []}).scrollOffsets, []);
}, 'A ScrollTimeline created with empty scrollOffsets should resolve to []');

test(t => {
  let offsets = new ScrollTimeline({timeRange: 100, scrollOffsets: [CSS.percent(20), 'auto']}).scrollOffsets;
  assert_offsets_equal(offsets[0], CSS.percent(20));
  assert_offsets_equal(offsets[1], new CSSKeywordValue('auto'));
}, 'A ScrollTimeline created with last \'auto\' offset in scrollOffsets should be allowed.');

test(t => {
  let constructorFunc = function() {
    new ScrollTimeline({timeRange: 100, scrollOffsets: null})
  };
  assert_throws_js(TypeError, constructorFunc);
}, 'Creating a ScrollTimeline with an invalid scrollOffsets value should throw');

test(t => {
  let constructorFunc = function() {
    new ScrollTimeline({timeRange: 100, scrollOffsets: ['auto']})
  };
  assert_throws_js(TypeError, constructorFunc);
}, 'Creating a ScrollTimeline with an scrollOffsets value of [\'auto\'] should throw');

const gValidScrollOffsetValues = [
  CSS.px(0),
  CSS.percent(100).sub(CSS.px(80)),
];

const gValidScrollOffsetSuffixes = [
  // Relative lengths.
  'em',
  'ex',
  'ch',
  'rem',
  'vw',
  'vh',
  'vmin',
  'vmax',
  // Absolute lengths.
  'cm',
  'mm',
  'q',
  'in',
  'pc',
  'pt',
  'px',
  // Percentage.
  '%',
];

for (let offset of gValidScrollOffsetValues) {
  test(function() {
    const scrollTimeline = new ScrollTimeline(
        {timeRange: 100, scrollOffsets: [offset, offset]});

    // Special case for 'auto'. This is valid input because of CSSKeywordish,
    // but when read back we expect a real CSSKeywordValue.
    if (offset === 'auto')
      offset = new CSSKeywordValue('auto');

    assert_offsets_equal(scrollTimeline.scrollOffsets[0], offset);
    assert_offsets_equal(scrollTimeline.scrollOffsets[1], offset);
  }, '\'' + offset + '\' is a valid scroll offset value');
}

for (const suffix of gValidScrollOffsetSuffixes) {
  test(function() {
    const offset = new CSSUnitValue(75, suffix);
    const scrollTimeline = new ScrollTimeline(
        {timeRange: 100, scrollOffsets: [offset, offset]});

    assert_offsets_equal(scrollTimeline.scrollOffsets[0], offset);
    assert_offsets_equal(scrollTimeline.scrollOffsets[1], offset);
  }, '\'' + suffix + '\' is a valid scroll offset unit');
}

// These are deliberately incomplete, just a random sampling of invalid
// values/units.
const gInvalidScrollOffsetValues = [
  '',
  'calc(360deg / 4)',
  'left',
  '#ff0000',
  'rgb(0, 128, 0)',
  'url("http://www.example.com/pinkish.gif")',
  'this_is_garbage',
  CSS.number(0),
  // Multiple valid values.
  '100px 5%',
  // Values that would be valid if represented with CSS Typed OM:
   0,
   '10px',
   '10%',
   'calc(100% - 80px)',
];

const gInvalidScrollOffsetSuffixes = [
  'deg',
  's',
  'Hz',
  'dpi',
];

for (const offset of gInvalidScrollOffsetValues) {
  test(function() {
    const constructorFunc = function() {
      new ScrollTimeline(
          {timeRange: 100, scrollOffsets: ['0px', offset]})
    };
    assert_throws_js(TypeError, constructorFunc);
  }, formatOffset(offset) + ' is an invalid scroll offset value in scrollOffsets');
}

for (const suffix of gInvalidScrollOffsetSuffixes) {
  test(function() {
    const offset = '75' + suffix;
    const constructorFunc = function() {
      new ScrollTimeline(
          {timeRange: 100, scrollOffsets: ['0px', offset]});
    };
    assert_throws_js(TypeError, constructorFunc);
  }, '\'' + suffix + '\' is an invalid scroll offset unit in scrollOffsets');
}

const offset_target = document.createElement('div');

const gValidElementBasedScrollOffsetValues = [
  {target: offset_target},
  {target: offset_target, threshold: 0},
  {target: offset_target, threshold: 0.5},
  {target: offset_target, threshold: 1},
];

for (let offset of gValidElementBasedScrollOffsetValues) {
  test(function() {
    const scrollTimeline = new ScrollTimeline(
        {timeRange: 100, scrollOffsets: [offset, offset]});

    // Special case unspecified threshold since it gets initialized to 0.
    if (!offset.hasOwnProperty('threshold'))
      offset.threshold = 0;

    assert_equals(scrollTimeline.scrollOffsets[0].target, offset.target);
    assert_equals(scrollTimeline.scrollOffsets[0].threshold, offset.threshold);
    assert_equals(scrollTimeline.scrollOffsets[1].target, offset.target);
    assert_equals(scrollTimeline.scrollOffsets[1].threshold, offset.threshold);
  }, '\'' + JSON.stringify(offset) + '\' is a valid scroll offset value');
}

const gInvalidElementBasedScrollOffsetValues = [
  {}, // empty
  {target: offset_target, threshold: "test"},
  {target: offset_target, threshold: 2},
  {target: offset_target, threshold: -0.2},
];

for (let offset of gInvalidElementBasedScrollOffsetValues) {
  test(function() {
    const constructorFunc = function() {
      new ScrollTimeline(
          {timeRange: 100, scrollOffsets: [offset]})
    };
    assert_throws_js(TypeError, constructorFunc);
  }, '\'' + JSON.stringify(offset) + '\' is an invalid scroll offset value in scrollOffsets');
}



// timeRange

test(function() {
  assert_equals(new ScrollTimeline().timeRange, 'auto');
}, 'A ScrollTimeline created with the default timeRange should default to \'auto\'');

const gValidTimeRangeValues = [
  'auto',
  0,
  -100,
  100,
  1234.5678,
];

for (let timeRange of gValidTimeRangeValues) {
  test(function() {
    const scrollTimeline = new ScrollTimeline({timeRange: timeRange});
    assert_equals(scrollTimeline.timeRange, timeRange);
  }, '\'' + timeRange + '\' is a valid timeRange value');
}

const gInvalidTimeRangeValues = [
  'invalid',
  Infinity,
  -Infinity,
  NaN,
];

for (let timeRange of gInvalidTimeRangeValues) {
  test(function() {
    const constructorFunc = function() {
      new ScrollTimeline({timeRange: timeRange});
    };
    assert_throws_js(TypeError, constructorFunc);
  }, '\'' + timeRange + '\' is an invalid timeRange value');
}
</script>
